using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom;
using System.Web.Services.Description;
using System.IO;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Reflection;
using System.Xml.Serialization;

namespace AJAXServiceProxyGeneration
{
    /// <summary>
    /// Generates Script# (http://www.codeplex.com/scriptsharp) client code
    /// </summary>
    public class ScriptSharpGenerator : ServiceGenerator
    {
        private string FAKE_FIELD = "RemoveTheFake";

        /// <summary>
        /// Contructor
        /// </summary>
        /// <param name="serviceNamespace">The Service Namespace</param>
        /// <param name="wsdlUri">The Uri to the Web Service to generate files against</param>
        public ScriptSharpGenerator(string serviceNamespace,string pocoNamespace, string wsdlUri,string excludes) : base(serviceNamespace, pocoNamespace,wsdlUri)
        {   
            this.ExcludeNames.AddRange(excludes.Split(','));
        }
        protected List<Type> ignoreTypes = new List<Type>(new Type[]
            {
                typeof(System.Xml.Serialization.XmlSerializerNamespaces),
                typeof(System.Xml.XmlQualifiedName)
            }
            );

        private List<string> ExcludeNames = new List<string>();

        private CodeTypeDeclaration _exceptionDeclaration;

        protected override void  ManipulateCodeDom(CodeNamespace codeNamespace)
        {
            //Create Exception Class
            this._exceptionDeclaration = new CodeTypeDeclaration("WebServiceException");
            this._exceptionDeclaration.Name = "WebServiceException";
            codeNamespace.Types.Add(_exceptionDeclaration);

            //Add Fields
            this._exceptionDeclaration.Members.Add(this.NewMemberField(typeof(string), "_stackTrace"));
            this._exceptionDeclaration.Members.Add(this.NewMemberField(typeof(string), "_message"));
            this._exceptionDeclaration.Members.Add(this.NewMemberField(typeof(string), "_statusCode"));
            this._exceptionDeclaration.Members.Add(this.NewMemberField(typeof(string), "_exceptionType"));
            this._exceptionDeclaration.Members.Add(this.NewMemberField(typeof(string), "_timeOut"));

            //Assuming the first type is the service declaration
            codeNamespace.Types[0].BaseTypes.Clear();
           
            if( !String.IsNullOrEmpty( codeNamespace.Name ) && codeNamespace.Types.Count>0)
            {
                codeNamespace.Types[0].Name = codeNamespace.Types[0].Name.Replace(codeNamespace.Name.Replace(".",string.Empty),string.Empty);
            }

            
            /*
             * We only need the imported attribute on the webservice;
             * 
             * Putting it on the Schema Types will not allow the contruction
             * of the type.
             */
            
             int count = 0;
             foreach (CodeTypeDeclaration dec in codeNamespace.Types)
             {
                 if (count == 0)
                 {
                     this.GenerateServiceInterface(dec);
                     this.GenerateServiceInstance(dec);
                     this.GenerateStaticService(dec);
                 }
                 else
                 {
                     if( !this.ExcludeNames.Contains(dec.Name))
                     {
                         CodeTypeDeclaration newCodeTypeDeclaration = new CodeTypeDeclaration(dec.Name);
                         newCodeTypeDeclaration.IsClass = true;
                         newCodeTypeDeclaration.IsEnum = dec.IsEnum;
                         newCodeTypeDeclaration.BaseTypes.AddRange(dec.BaseTypes);
                         newCodeTypeDeclaration.CustomAttributes.Add(new CodeAttributeDeclaration("Imported"));
                         newCodeTypeDeclaration.Comments.Add(new CodeCommentStatement("<summary>" + dec.Name + "</summary>",true));
                         this.GenerateType(this.POCONamespace, this.POCOClassGeneratorWriter, dec, newCodeTypeDeclaration);
                     }
                 }
                 count++;
             }
            
        }

        /// <summary>
        /// Creates a New Public Member field
        /// </summary>
        /// <param name="type"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        private CodeMemberField NewMemberField(Type type, string name)
        {
            CodeMemberField memberField = new CodeMemberField(type, name);
            memberField.Attributes = MemberAttributes.Public;
            return memberField;
        }

        private CodeTypeDeclaration GenerateServiceInterface(CodeTypeDeclaration codeTypeDeclaration)
        {
            CodeTypeDeclaration ctdI = new CodeTypeDeclaration("I" + codeTypeDeclaration.Name);
            ctdI.Comments.Add(new CodeCommentStatement("<summary>Interface for invoking the " + codeTypeDeclaration.Name + "</summary>", true));

            ctdI.IsInterface = true;
            ctdI.IsClass = false;
            this.GenerateType(this.ServiceNamespace, this.ServiceClassGeneratorWriter, codeTypeDeclaration, ctdI,true,false,false);

            return ctdI;
        }

        private CodeTypeDeclaration GenerateServiceInstance(CodeTypeDeclaration codeTypeDeclaration)
        {
            CodeTypeDeclaration ctdI = new CodeTypeDeclaration(codeTypeDeclaration.Name + "Impl");
            ctdI.Comments.Add(new CodeCommentStatement("<summary>Service Implementation for invoking the static service <see ref=\"" + codeTypeDeclaration.Name + "\"/></summary>", true));

            ctdI.IsInterface = false;
            ctdI.IsClass = true;
            ctdI.TypeAttributes = TypeAttributes.Sealed | TypeAttributes.Public;
            ctdI.Name = codeTypeDeclaration.Name + "Impl";
            ctdI.BaseTypes.Add("I" + codeTypeDeclaration.Name);
        
            this.GenerateType(this.ServiceNamespace, this.ServiceClassGeneratorWriter, codeTypeDeclaration, ctdI,false,true,false);

            return ctdI;

        }

        private CodeTypeDeclaration GenerateStaticService(CodeTypeDeclaration codeTypeDeclaration)
        {
            CodeTypeDeclaration ctdI = new CodeTypeDeclaration(codeTypeDeclaration.Name);

            ctdI.Comments.Add(new CodeCommentStatement("<summary>Static Service " + codeTypeDeclaration.Name + "</summary>", true));

            ctdI.IsInterface = false;
            ctdI.IsClass = true;
            ctdI.TypeAttributes = TypeAttributes.NestedAssembly;
            ctdI.CustomAttributes.Add(new CodeAttributeDeclaration("IgnoreNamespace"));
            ctdI.CustomAttributes.Add(new CodeAttributeDeclaration("Imported"));
            this.GenerateType(this.ServiceNamespace, this.ServiceClassGeneratorWriter, codeTypeDeclaration,ctdI);

            return ctdI;

        }

        protected virtual void GenerateType(string @namespace, IGeneratorWriter generatorWriter, CodeTypeDeclaration existingCodeTypeDeclaration, CodeTypeDeclaration newCodeTypeDeclaration)
        {
            this.GenerateType(@namespace, generatorWriter, existingCodeTypeDeclaration,newCodeTypeDeclaration,false,false,true);
        }

        /// <summary>
        /// Generates a class file for the specified Type
        /// </summary>
        /// <param name="codeTypeDeclaration"></param>
        protected virtual void GenerateType(string @namespace,IGeneratorWriter generatorWriter, CodeTypeDeclaration existingCodeTypeDeclaration, CodeTypeDeclaration newCodeTypeDeclaration, bool generateMethodDelegates, bool createStaticInstanceInvocationBody, bool preserveCase)
        {
            CodeNamespace codeNamespace = new CodeNamespace(@namespace);

            codeNamespace.Imports.Add(new CodeNamespaceImport("Imported = System.ImportedAttribute")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("PreserveCase = System.PreserveCaseAttribute")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("IgnoreNamespace = System.IgnoreNamespaceAttribute")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("IntrinsicProperty = System.IntrinsicPropertyAttribute")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("String = System.String")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("Object = System.Object")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("Array = System.Array")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("DateTime = System.DateTime")); //Script# mscorlib alternative
            codeNamespace.Imports.Add(new CodeNamespaceImport("SysException = System.Exception")); //Script# mscorlib alternative
            
            codeNamespace.Imports.Add(new CodeNamespaceImport(this.POCONamespace));
            codeNamespace.Comments.Add(new CodeCommentStatement(string.Format("{0} UTC - Generated using AJAXCodeGenerator (http://ajaxcodegeneration.codeplex.com/)", DateTime.UtcNow)));
            codeNamespace.Types.Add(newCodeTypeDeclaration);
            //Find the namespace attribute
            foreach (CodeAttributeDeclaration cad in existingCodeTypeDeclaration.CustomAttributes)
            {
                if (cad.Name.Equals(typeof(XmlTypeAttribute).FullName))
                {
                    foreach (CodeAttributeArgument caa in cad.Arguments)
                    {
                        if (caa.Name.Equals("namespace", StringComparison.OrdinalIgnoreCase))
                        {
                            @namespace = ((CodePrimitiveExpression)caa.Value).Value.ToString();
                        }
                    }
                }
            }


            CodeTypeMember member;

            for (int m = existingCodeTypeDeclaration.Members.Count - 1; m >= 0; m--)
            {
                member = existingCodeTypeDeclaration.Members[m];
                
                if (typeof(CodeConstructor).IsInstanceOfType(member))
                {
                    //Skip contructors
                    continue;
                }
                if (typeof(CodeMemberMethod).IsInstanceOfType(member))
                {
                    //Modify method
                    this.GenerateMethod(codeNamespace, existingCodeTypeDeclaration, newCodeTypeDeclaration, (CodeMemberMethod)member, generateMethodDelegates, createStaticInstanceInvocationBody,preserveCase);
                }
                else if (typeof(CodeMemberField).IsInstanceOfType(member))
                {

                    CodeMemberField mf = (CodeMemberField)member;
                    if (mf.Name.Equals(FAKE_FIELD))
                    {
                        continue;
                    }

                    if (existingCodeTypeDeclaration.IsEnum)
                    {
                        mf.Name = string.Format("{0} = {1}", mf.Name, m);
                        this.PreserveCaseOnMember(newCodeTypeDeclaration, mf);
                    }
                    else
                    {

                        if (this.ignoreTypes.ConvertAll<string>(new Converter<Type, string>(delegate(Type type) { return type.FullName; })).Contains(mf.Type.BaseType))
                        {
                            continue;
                        }
                        this.PreserveCaseOnMember(newCodeTypeDeclaration, mf);
                    }
                }
                else
                {
                    this.PreserveCaseOnMember(newCodeTypeDeclaration, member);
                }
            }


            this.WriteFile(codeNamespace, generatorWriter, newCodeTypeDeclaration.Name);
        }

        /// <summary>
        /// Writes out the <paramref name="codeNamespace"/> to a file
        /// named <paramref name="fileName"/>.cs
        /// </summary>
        /// <param name="codeNamespace">CodeNamespace to write to file</param>
        /// <param name="fileName">Name of the file</param>
        protected virtual void WriteFile(CodeNamespace codeNamespace, IGeneratorWriter generatorWriter, string fileName)
        {
            Stream ms = new MemoryStream();
            StreamWriter sw = new StreamWriter(ms);
            
            CodeDomProvider codeProvider = new CSharpCodeProvider();
            codeProvider.GenerateCodeFromNamespace(codeNamespace, sw, null);
            sw.Flush();
            generatorWriter.Write(fileName, ms);
        }

        /// <summary>
        /// Adds the PreserveCase attribute to members
        /// </summary>
        /// <param name="member"></param>
        protected virtual void PreserveCaseOnMember(CodeTypeDeclaration newCodeTypeDeclaration, CodeTypeMember member)
        {
            if (typeof(CodeMemberField).IsInstanceOfType(member)) //Field
            {
                //Upper Camel Case Field
                CodeMemberField field = (CodeMemberField)member;
               
                string name = field.Name.Substring(0, 1).ToUpper() + field.Name.Substring(1, field.Name.Length - 1);
                if (field.Name.Equals("case", StringComparison.OrdinalIgnoreCase))
                {
                    name = "@" + field.Name;
                }
                if (field.Type.ArrayElementType != null)
                {
                    if (!name.EndsWith("Collection", StringComparison.InvariantCultureIgnoreCase))
                    {
                        name += "Collection";
                    }
                }

                if (newCodeTypeDeclaration.IsEnum)
                {
                    CodeMemberField newField = new CodeMemberField(this.SubstituteTypes(field.Type), field.Name);

                    newField.Comments.Add(new CodeCommentStatement("<summary>" + field.Name + "</summary>", true));
                    newField.CustomAttributes.Add(new CodeAttributeDeclaration("PreserveCase"));

                    newCodeTypeDeclaration.Members.Add(newField);
                }
                else
                {

                    CodeMemberProperty property = new CodeMemberProperty();
                    property.Name = field.Name.Substring(0, 1).ToUpper() + field.Name.Substring(1, field.Name.Length - 1);
                    if (field.Name.Equals("case", StringComparison.OrdinalIgnoreCase))
                    {
                        property.Name = "@" + field.Name;
                    }
                    property.Comments.Add(new CodeCommentStatement("<summary>" + field.Name + "</summary>", true));
                    if (field.Type.ArrayElementType != null)
                    {
                        if (!property.Name.EndsWith("Collection", StringComparison.InvariantCultureIgnoreCase))
                        {
                            property.Name += "Collection";
                        }
                    }
                    property.CustomAttributes.Add(new CodeAttributeDeclaration("PreserveCase"));
                    property.CustomAttributes.Add(new CodeAttributeDeclaration("IntrinsicProperty"));
                    property.Type = this.SubstituteTypes(field.Type);
                    property.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                    property.HasGet = true;
                    property.HasSet = true;
                     
                    CodeThrowExceptionStatement thro = new CodeThrowExceptionStatement(
                        new CodeMethodInvokeExpression(
                            new CodeTypeReferenceExpression("SysException")
                            , "NotImplemented")
                    );

                    property.GetStatements.Add(thro);
                    property.SetStatements.Add(thro);
                    newCodeTypeDeclaration.Members.Add(property);
                }
            }
        }

        /// <summary>
        /// Replace Script# incompitible types with suitable types
        /// </summary>
        /// <param name="codeTypeReference"></param>
        /// <returns></returns>
        protected virtual CodeTypeReference SubstituteTypes(CodeTypeReference codeTypeReference)
        {
            Type type = Type.GetType(codeTypeReference.BaseType);

            if( type!=null)
            {
                if( type == typeof(Nullable<>))
                {
                    string typeName =this.SubstituteTypesName(codeTypeReference.TypeArguments[0].BaseType);
                    codeTypeReference.TypeArguments.Clear();
                    if (codeTypeReference.ArrayElementType != null)
                        if(Type.GetType(codeTypeReference.ArrayElementType.BaseType) != typeof(Nullable<>))
                        codeTypeReference.ArrayElementType = this.SubstituteTypes(codeTypeReference.ArrayElementType);
                        else
                            codeTypeReference.ArrayElementType.BaseType = typeName;
                        codeTypeReference.BaseType = typeName;
                }
                else if (codeTypeReference.ArrayElementType != null)
                {
                    codeTypeReference.ArrayElementType.BaseType = this.SubstituteTypesName(codeTypeReference.ArrayElementType.BaseType);
                }
                
                codeTypeReference.BaseType = this.SubstituteTypesName(codeTypeReference.BaseType);
            }
            
            return codeTypeReference;
        }

        /// <summary>
        /// Replace Script# incompitible types with sutiable types
        /// </summary>
        /// <param name="typeName"></param>
        /// <returns></returns>
        protected virtual string SubstituteTypesName(string typeName)
        {
            Type type = Type.GetType(typeName);
            
            if( type==null)
                return typeName;

            if( type == typeof(Guid))
            {
                return typeof(string).FullName;
            }
            if( type== typeof(DateTime))
            {
                return "DateTime";
            }
            if (type == typeof(TimeSpan))
            {
                return typeof(string).FullName;
            }
            return typeName;
        }

        /// <summary>
        /// Modifies the method
        /// </summary>
        /// <param name="codeNamespace"></param>
        /// <param name="method"></param>
        private void GenerateMethod(CodeNamespace codeNamespace,CodeTypeDeclaration existingCodeTypeDeclaration, CodeTypeDeclaration newCodeTypeDeclaration, CodeMemberMethod method, bool generateDelegate, bool createStaticInstanceInvocationBody, bool preserveCase)
        {
            CodeMemberMethod newMethod = new CodeMemberMethod();
            newMethod.Name = method.Name;
            newMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            //We found a method; now modify it
            //Remove Attributes because they will not be needed for
            //and AJAX Client
            if( preserveCase)
                newMethod.CustomAttributes.Add(new CodeAttributeDeclaration("PreserveCase"));
            
            //Modify Parameters
            foreach(CodeParameterDeclarationExpression parameter in method.Parameters) 
            {
                CodeParameterDeclarationExpression newParameter = new CodeParameterDeclarationExpression();
                newParameter.Name = parameter.Name;
                newParameter.Type = this.SubstituteTypes(parameter.Type);
                newMethod.Parameters.Add(newParameter);
            }
            //Create success delegate
            CodeTypeDelegate successDelegate = this.GenerateSuccessHandlerDelegate(newCodeTypeDeclaration, method);
            successDelegate.Comments.Add(new CodeCommentStatement("<summary>Success Delegate for the method " + method.Name + "</summary>", true));
            CodeParameterDeclarationExpression successParameter = new CodeParameterDeclarationExpression(successDelegate.Name, "successCallbackHandler");
            newMethod.Parameters.Add(successParameter);

            //Create failure delegate
            CodeTypeDelegate failureDelegate = this.GenerateFailureHandlerDelegate(newCodeTypeDeclaration, method);
            failureDelegate.Comments.Add(new CodeCommentStatement("<summary>Failure Delegate for the method " + method.Name + "</summary>", true));

            CodeParameterDeclarationExpression failureParameter = new CodeParameterDeclarationExpression(failureDelegate.Name, "failureCallbackHandler");
            newMethod.Parameters.Add(failureParameter);

            //User context
            CodeParameterDeclarationExpression userContext = new CodeParameterDeclarationExpression();
            userContext.Name = "userContext";
            userContext.Type = new CodeTypeReference(typeof(object));
            newMethod.Parameters.Add(userContext);
            
            if (generateDelegate)
            {
                codeNamespace.Types.Add(successDelegate);
                codeNamespace.Types.Add(failureDelegate);
            }

            if (createStaticInstanceInvocationBody)
            {
                List<CodeExpression> expressions = new List<CodeExpression>();
                foreach (CodeParameterDeclarationExpression par in newMethod.Parameters)
                {

                    expressions.Add(new CodeVariableReferenceExpression(par.Name));
                }
                //Make the method static
                newMethod.Statements.Add(new CodeMethodInvokeExpression(
                     new CodeTypeReferenceExpression(existingCodeTypeDeclaration.Name)
                            , method.Name
                            , expressions.ToArray()));

                newMethod.Comments.Add(new CodeCommentStatement(
                    string.Format("<summary>Performs an invoke on the <see ref=\"{0}.{1}\"/></summary>", existingCodeTypeDeclaration.Name, method.Name)
                    , true));

            }
            else
            {
                //Make the method static
                newMethod.Attributes = MemberAttributes.Static | MemberAttributes.Public;
                newMethod.Comments.Add(new CodeCommentStatement("<summary>No method body is intended</summary>", true));

            }
            
            
            //Modify Method Signiture by adding the callback handlers
            newMethod.ReturnType = new CodeTypeReference(typeof(void));

            newCodeTypeDeclaration.Members.Add( newMethod );
        }

        private List<string> successHandlerNames = new List<string>();
        private List<string> failureHandlerNames = new List<string>();

        private Dictionary<string, int> methodNameClasses = new Dictionary<string, int>();

        private string GetDelegateName(CodeTypeDeclaration dec, string name, bool register)
        {
            if (methodNameClasses.ContainsKey(name))
            {
                string newname = name;
                
                if( register && dec.IsInterface)
                    methodNameClasses[name] = methodNameClasses[name]+1;

                if (methodNameClasses[name] > 0)
                    newname = name + methodNameClasses[name];

                return newname;
            }
            else
            {
                if( dec.IsInterface)
                    methodNameClasses.Add(name, 0);
            }
            return name;
        }
        /// <summary>
        /// Generates the Success Callback Handler Delegate
        /// </summary>
        /// <param name="codeNamespace"></param>
        /// <param name="methodName"></param>
        /// <param name="returnType"></param>
        /// <returns></returns>
        protected virtual CodeTypeDelegate GenerateSuccessHandlerDelegate(CodeTypeDeclaration codeTypeDeclaration, CodeMemberMethod method)
        {
            string name = this.GetDelegateName( codeTypeDeclaration, method.Name , true);

            CodeTypeDelegate codeTypeDelegate = new CodeTypeDelegate(
                string.Concat(name, "SuccessCallbackDelegate")
                );

            if(     Type.GetType(method.ReturnType.BaseType)!=typeof(void) && 
                    this.SubstituteTypes(method.ReturnType).Equals(method.ReturnType) )
            {
                codeTypeDelegate.Parameters.Add(new CodeParameterDeclarationExpression(method.ReturnType,string.Concat(name, "Result")));
            }
            codeTypeDelegate.Parameters.Add(new CodeParameterDeclarationExpression(typeof(object), "userContent"));
            codeTypeDelegate.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "methodName"));
            codeTypeDelegate.CustomAttributes.Add(new CodeAttributeDeclaration("Imported"));

            return codeTypeDelegate;
        }

       
        /// <summary>
        /// Generates the Failure Callback Handler Delegate
        /// </summary>
        /// <param name="codeNamespace"></param>
        /// <param name="methodName"></param>
        /// <returns></returns>
        protected virtual CodeTypeDelegate GenerateFailureHandlerDelegate(CodeTypeDeclaration codeTypeDeclaration, CodeMemberMethod method)
        {
            string name = this.GetDelegateName(codeTypeDeclaration, method.Name, false);
            CodeTypeDelegate codeTypeDelegate = new CodeTypeDelegate(string.Concat(name, "FailureCallbackDelegate"));

            codeTypeDelegate.Parameters.Add(new CodeParameterDeclarationExpression(_exceptionDeclaration.Name, "exception"));
            codeTypeDelegate.CustomAttributes.Add(new CodeAttributeDeclaration("Imported"));
                
            return codeTypeDelegate;
        }
    }
}
